package com.kamistoat.meimeistore.commonlog.aspect;

import com.alibaba.fastjson2.JSON;
import com.kamistoat.meimeistore.commonlog.annotation.Log;
import com.kamistoat.meimeistore.commonlog.enums.BusinessStatus;
import com.kamistoat.meimeistore.commonlog.utils.AsyncLogService;
import com.kamistoat.meimeistore.apisystem.domain.SysOperLog;
import com.kamistoat.meimeistore.commoncore.utils.ServletUtils;
import com.kamistoat.meimeistore.commoncore.utils.StringUtils;
import com.kamistoat.meimeistore.commoncore.utils.ip.IpUtils;
import com.kamistoat.meimeistore.commonsecurity.utils.SecurityUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.validation.BindingResult;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Collection;
import java.util.Map;

/**
 * 使用切面技术进行操作日志记录。
 * AOP技术的一个常用场景就是 日志记录
 */
@Aspect
@Component
public class LogAspect {
    private static final Logger LOGGER = LoggerFactory.getLogger(LogAspect.class);

    @Autowired
    AsyncLogService asyncLogService;

    /**
     * 切入点是当被 @Log 注解标注的方法执行完毕后.
     * TODO 这里有个问题，那就是如果@Log标注 Controller层方法。方法返回的是Object，但是受到 @ResponseBody的影响返回前端的其实是JSON。
     * TODO 那么当切面 AfterReturning执行时，获取的 returning到底是 Object还是Json？猜测是Object
     *
     * @param joinPoint     切入点
     * @param logAnnotation @Log
     * @param jsonResult    被 @Log 注解标注的方法的放回值
     */
    @AfterReturning(value = "@annotation(logAnnotation)", returning = "jsonResult")
    public void doAfterReturning(JoinPoint joinPoint, Log logAnnotation, Object jsonResult) {
        handleLog(joinPoint, logAnnotation, null, jsonResult);
    }

    /**
     * 记录日志，包括将日志持久化
     *
     * @param joinPoint     发送记录日志行为的切点
     * @param logAnnotation @Log注解
     * @param exception
     * @param jsonResult
     */
    protected void handleLog(final JoinPoint joinPoint,
                             Log logAnnotation,
                             final Exception exception,
                             Object jsonResult) {
        try {
            /**
             * 持久化日志到数据库
             */
            // 创建系统日志的vo类，最后再将vo类拷贝为Entity保存到数据库中
            SysOperLog sysOperLog = new SysOperLog();

            // 设置默认的状态码。注意这里使用了枚举类自带的方法，获取枚举变量在枚举类中定义的顺序
            sysOperLog.setStatus(BusinessStatus.SUCCESS.ordinal());

            // 保存ip/url等相关参数
            String ipAddr = IpUtils.getIpAddr();
            sysOperLog.setOperIp(ipAddr);
            sysOperLog.setOperUrl(ServletUtils.getRequest().getRequestURI());

            // 从ThreadLocal中获取userName.当其不为空且不全是空格的时候
            String userName = SecurityUtils.getUserName();
            if (StringUtils.isNotBlank(userName)) {
                sysOperLog.setOperName(userName);
            }

            // 如果存在异常则日志的状态码为FAIL，且截取2000字符之内的错误进行记录
            if (exception != null) {
                sysOperLog.setStatus(BusinessStatus.FAIL.ordinal());
                sysOperLog.setErrorMsg(StringUtils.substring(exception.getMessage(), 0, 2000));
            }

            // JoinPoint.getTarget()获得切点目标所在的对象。注意理解这里的切点目标，它是指当切点条件被触发时，条件之下的内容。
            // 像这里就是切点条件是 @Log，切点目标就是被 @Log注解标注的方法，而getTarget()就是获取被标注的方法所在的对象
            // 在用一个 getClass()，就获得了对象的元类信息
            String className = joinPoint.getTarget().getClass().getName();
            // 获取切点的方法名
            String methodName = ((MethodSignature) joinPoint.getSignature()).getName();
            // 保存方法名
            sysOperLog.setMethod(className + "." + methodName + "()");

            // 保存请求方式
            sysOperLog.setRequestMethod(ServletUtils.getRequest().getMethod());

            // 处理 @Log 注解内的参数
            getControllerMethodDescription(joinPoint, logAnnotation, sysOperLog, jsonResult);

            // 将日志保存到数据库。注意传递的仍然是 vo 类
            asyncLogService.saveSysLog(sysOperLog);
        } catch (Exception e) {
            /**
             * 持久化日志出现异常，则只能通过本地日志进行输出
             */
            // 记录本地异常日志
            LOGGER.error("==前置通知异常==");
            LOGGER.error("异常信息:{}", e.getMessage());
            e.printStackTrace();
        }
    }

    /**
     * 处理 @Log注解内的参数
     *
     * @param joinPoint     切点
     * @param logAnnotation @Log 注解
     * @param sysOperLog    SysOperLog的vo类
     * @param jsonResult    原方法的返回结果
     * @throws Exception
     */
    private void getControllerMethodDescription(JoinPoint joinPoint,
                                                Log logAnnotation,
                                                SysOperLog sysOperLog,
                                                Object jsonResult)
            throws Exception {
        // 将 @Log 注解中的简单参数进行保存
        sysOperLog.setBusinessType(logAnnotation.businessType().ordinal());
        sysOperLog.setTitle(logAnnotation.title());
        sysOperLog.setOperatorType(logAnnotation.operatorType().ordinal());

        /**
         * 如果 @Log 中选择保存Request参数，则保存之
         */
        if (logAnnotation.isSaveRequestData()) {
            setRequestValue(joinPoint, sysOperLog);
        }

        /**
         * 如果 @Log 中选择保存Response参数，则截取前2000长度进行保存
         */
        if (logAnnotation.isSaveResponseData()) {
            // 将 Object转换成 JSON后保存
            sysOperLog.setJsonResult(StringUtils.substring(JSON.toJSONString(jsonResult), 0, 2000));
        }
    }

    /**
     * 如果 @Log选择保存 Request参数，则提取 Request中的参数进行保存
     *
     * @param joinPoint  切点
     * @param sysOperLog SysOperLog的vo类
     */
    private void setRequestValue(JoinPoint joinPoint, SysOperLog sysOperLog) throws Exception {
        String requestMethod = sysOperLog.getRequestMethod();
        // 原项目中只对 PUT和POST请求的Request保存参数。删掉。由Controller层觉得到底那些请求要保存Request。这里无条件保存
        // if(HttpMethod.PUT.name().equals(requestMethod) || HttpMethod.POST.name().equals(requestMethod)){}

        // 拼接Request参数并保存到 SysOperLog中.
        // 这里有一个涉及到切点源码的知识，joinPoint.getArgs()会将到达切点前的整个调用链中的所有参数返回。
        // 因此在Controller中，joinPoint.getArgs()[0]就是request，因为Request必然是开启这个调用链的首位
        String params = argsArrayToString(joinPoint.getArgs());
        sysOperLog.setOperParam(StringUtils.substring(params, 0, 2000));
    }

    /**
     * 对 joinPoint.getArgs()的参数数组进行拼接。这里会进行过滤，只有Request相关的参数才进行拼接
     *
     * @param params joinPoint.getArgs()返回的到达切点前的整个调用链中的所有参数组成的数组。
     * @return 拼接结果
     */
    private String argsArrayToString(Object... params) {
        StringBuilder resParam = new StringBuilder();
        if (StringUtils.isNotNull(params) && params.length > 0) {
            for (Object param : params) {
                // 只有当这个参数在日志记录中不应该被忽略，才进行拼接
                if (StringUtils.isNotNull(param) && !isIgnoredParam(param)) {
                    try {
                        resParam.append(JSON.toJSONString(param));
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }
        }
        return resParam.toString().trim();
    }

    /**
     * 判断 joinPoint.getArgs()中的某个参数，是否是日志记录中要忽略的参数
     *
     * @param param joinPoint.getArgs()中的某个参数
     * @return 如果要忽略则返回true，否则返回false
     */
    private boolean isIgnoredParam(Object param) {
        // 获取参数元类
        Class<?> clazz = param.getClass();

        if (clazz.isArray()) {
            // 忽略掉文件上传的文件分片(数组形式)
            // clazz.getComponentType()--如果clazz是Array则返回内部元素元类
            return clazz.getComponentType().isAssignableFrom(MultipartFile.class);
        } else if (Collection.class.isAssignableFrom(clazz)) {
            // 忽略文件上传的文件分片(集合形式)
            Collection collection = (Collection) param;
            for (Object value : collection) {
                return value instanceof MultipartFile;
            }
        } else if (Map.class.isAssignableFrom(clazz)) {
            // 忽略文件上传的文件分片(Map形式)
            Map map = (Map) param;
            for (Object value : map.entrySet()) {
                Map.Entry entry = (Map.Entry) value;
                return entry.getValue() instanceof MultipartFile;
            }
        }
        // 忽略文件上传、HttpServletRequest、HttpServletResponse、BindingResult 四种单纯的元类
        // 保留剩下的全部参数
        return param instanceof MultipartFile
                || param instanceof HttpServletRequest
                || param instanceof HttpServletResponse
                || param instanceof BindingResult;
    }
}
